<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2">
<meta name="theme-color" content="#222">
<meta name="generator" content="Hexo 4.2.1">
  <link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon-next.png">
  <link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32-next.png">
  <link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16-next.png">
  <link rel="mask-icon" href="/images/logo.svg" color="#222">
  <meta name="google-site-verification" content="yF8HXsFuCTrePDFRnSkN2kkzV2ypBjtywzXsfVXOOV8">
  <meta name="baidu-site-verification" content="VxrVAiQAZ9">

<link rel="stylesheet" href="/css/main.css">


<link rel="stylesheet" href="/lib/font-awesome/css/all.min.css">

<script id="hexo-configurations">
    var NexT = window.NexT || {};
    var CONFIG = {"hostname":"yang0033.gitee.io","root":"/","scheme":"Gemini","version":"7.8.0","exturl":false,"sidebar":{"position":"left","display":"post","padding":18,"offset":12,"onmobile":false},"copycode":{"enable":true,"show_result":true,"style":null},"back2top":{"enable":true,"sidebar":false,"scrollpercent":false},"bookmark":{"enable":false,"color":"#222","save":"auto"},"fancybox":false,"mediumzoom":false,"lazyload":false,"pangu":false,"comments":{"style":"tabs","active":null,"storage":true,"lazyload":false,"nav":null},"algolia":{"hits":{"per_page":10},"labels":{"input_placeholder":"Search for Posts","hits_empty":"We didn't find any results for the search: ${query}","hits_stats":"${hits} results found in ${time} ms"}},"localsearch":{"enable":true,"trigger":"auto","top_n_per_article":1,"unescape":true,"preload":true},"motion":{"enable":true,"async":false,"transition":{"post_block":"fadeIn","post_header":"slideDownIn","post_body":"slideDownIn","coll_header":"slideLeftIn","sidebar":"slideUpIn"}},"path":"search.xml"};
  </script>

  <meta name="description" content="FRP是异步数据流编程 这不是什么新鲜的东西了。在前端编程中（用Javascript），监听某个按钮的点击事件，并在事件被触发以后回调一个函数做一些操作，这个过程就是异步数据流编程，也就是FRP。FRP的灵感来源于细胞的激素刺激，你可以回想一下初中生物学的“生物应激”。我们可以为任何东西创建数据流（Stream），不仅仅局限于click和hover事件。Stream是随处可见的，任何东西都可以成为">
<meta property="og:type" content="article">
<meta property="og:title" content="FRP-实例简介(RxJS-RxSwift-ReactiveCocoa)">
<meta property="og:url" content="http://yang0033.gitee.io/2017/03/10/FRP-%E5%AE%9E%E4%BE%8B%E7%AE%80%E4%BB%8B(RxJS---RxSwift---ReactiveCocoa)/index.html">
<meta property="og:site_name" content="SuperYang&#96;s Blog">
<meta property="og:description" content="FRP是异步数据流编程 这不是什么新鲜的东西了。在前端编程中（用Javascript），监听某个按钮的点击事件，并在事件被触发以后回调一个函数做一些操作，这个过程就是异步数据流编程，也就是FRP。FRP的灵感来源于细胞的激素刺激，你可以回想一下初中生物学的“生物应激”。我们可以为任何东西创建数据流（Stream），不仅仅局限于click和hover事件。Stream是随处可见的，任何东西都可以成为">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://camo.githubusercontent.com/36c0a9ffd8ed22236bd6237d44a1d3eecbaec336/687474703a2f2f692e696d6775722e636f6d2f634c344d4f73532e706e67">
<meta property="og:image" content="https://camo.githubusercontent.com/995c301de2f566db10748042a5a67cc5d9ac45d9/687474703a2f2f692e696d6775722e636f6d2f484d47574e4f352e706e67">
<meta property="og:image" content="https://camo.githubusercontent.com/81e5d63c69768e1b04447d2e246f47540dd83fbd/687474703a2f2f692e696d6775722e636f6d2f65416c4e62306a2e706e67">
<meta property="og:image" content="https://camo.githubusercontent.com/2a8a9cc75acd13443f588fd7f386bd7a6dcb271a/687474703a2f2f692e696d6775722e636f6d2f48486e6d6c61632e706e67">
<meta property="og:image" content="https://camo.githubusercontent.com/0b0ac4a249e1c15d7520c220957acfece1af3e95/687474703a2f2f692e696d6775722e636f6d2f4869337a4e7a4a2e706e67">
<meta property="og:image" content="https://camo.githubusercontent.com/e581baffb3db3e4f749350326af32de8d5ba4363/687474703a2f2f692e696d6775722e636f6d2f4149696d5138432e6a7067">
<meta property="article:published_time" content="2017-03-09T16:24:00.000Z">
<meta property="article:modified_time" content="2020-07-09T08:00:12.305Z">
<meta property="article:author" content="SuperYang">
<meta property="article:tag" content="RAC">
<meta property="article:tag" content="FRP">
<meta property="article:tag" content="RxJS">
<meta property="article:tag" content="RxSwift">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://camo.githubusercontent.com/36c0a9ffd8ed22236bd6237d44a1d3eecbaec336/687474703a2f2f692e696d6775722e636f6d2f634c344d4f73532e706e67">

<link rel="canonical" href="http://yang0033.gitee.io/2017/03/10/FRP-%E5%AE%9E%E4%BE%8B%E7%AE%80%E4%BB%8B(RxJS---RxSwift---ReactiveCocoa)/">


<script id="page-configurations">
  // https://hexo.io/docs/variables.html
  CONFIG.page = {
    sidebar: "",
    isHome : false,
    isPost : true,
    lang   : 'zh-CN'
  };
</script>

  <title>FRP-实例简介(RxJS-RxSwift-ReactiveCocoa) | SuperYang`s Blog</title>
  
    <script async src="https://www.googletagmanager.com/gtag/js?id=UA-71812998-3"></script>
    <script>
      if (CONFIG.hostname === location.hostname) {
        window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('js', new Date());
        gtag('config', 'UA-71812998-3');
      }
    </script>


  <script>
    var _hmt = _hmt || [];
    (function() {
      var hm = document.createElement("script");
      hm.src = "https://hm.baidu.com/hm.js?8ff3295288514de6f89f314c23ee40ab";
      var s = document.getElementsByTagName("script")[0];
      s.parentNode.insertBefore(hm, s);
    })();
  </script>




  <noscript>
  <style>
  .use-motion .brand,
  .use-motion .menu-item,
  .sidebar-inner,
  .use-motion .post-block,
  .use-motion .pagination,
  .use-motion .comments,
  .use-motion .post-header,
  .use-motion .post-body,
  .use-motion .collection-header { opacity: initial; }

  .use-motion .site-title,
  .use-motion .site-subtitle {
    opacity: initial;
    top: initial;
  }

  .use-motion .logo-line-before i { left: initial; }
  .use-motion .logo-line-after i { right: initial; }
  </style>
</noscript>

</head>

<body itemscope itemtype="http://schema.org/WebPage">
  <div class="container use-motion">
    <div class="headband"></div>

    <header class="header" itemscope itemtype="http://schema.org/WPHeader">
      <div class="header-inner"><div class="site-brand-container">
  <div class="site-nav-toggle">
    <div class="toggle" aria-label="切换导航栏">
      <span class="toggle-line toggle-line-first"></span>
      <span class="toggle-line toggle-line-middle"></span>
      <span class="toggle-line toggle-line-last"></span>
    </div>
  </div>

  <div class="site-meta">

    <a href="/" class="brand" rel="start">
      <span class="logo-line-before"><i></i></span>
      <h1 class="site-title">SuperYang`s Blog</h1>
      <span class="logo-line-after"><i></i></span>
    </a>
      <p class="site-subtitle" itemprop="description">落魄程序员在线炒饭</p>
  </div>

  <div class="site-nav-right">
    <div class="toggle popup-trigger">
        <i class="fa fa-search fa-fw fa-lg"></i>
    </div>
  </div>
</div>




<nav class="site-nav">
  <ul id="menu" class="main-menu menu">
        <li class="menu-item menu-item-home">

    <a href="/" rel="section"><i class="fa fa-home fa-fw"></i>首页</a>

  </li>
        <li class="menu-item menu-item-about">

    <a href="/about/" rel="section"><i class="fa fa-user fa-fw"></i>关于</a>

  </li>
        <li class="menu-item menu-item-tags">

    <a href="/tags/" rel="section"><i class="fa fa-tags fa-fw"></i>标签</a>

  </li>
        <li class="menu-item menu-item-categories">

    <a href="/categories/" rel="section"><i class="fa fa-th fa-fw"></i>分类</a>

  </li>
        <li class="menu-item menu-item-archives">

    <a href="/archives/" rel="section"><i class="fa fa-archive fa-fw"></i>归档</a>

  </li>
        <li class="menu-item menu-item-message">

    <a href="/message/" rel="section"><i class="fa fa-music fa-fw"></i>留言板 | Music</a>

  </li>
      <li class="menu-item menu-item-search">
        <a role="button" class="popup-trigger"><i class="fa fa-search fa-fw"></i>搜索
        </a>
      </li>
  </ul>
</nav>



  <div class="search-pop-overlay">
    <div class="popup search-popup">
        <div class="search-header">
  <span class="search-icon">
    <i class="fa fa-search"></i>
  </span>
  <div class="search-input-container">
    <input autocomplete="off" autocapitalize="off"
           placeholder="搜索..." spellcheck="false"
           type="search" class="search-input">
  </div>
  <span class="popup-btn-close">
    <i class="fa fa-times-circle"></i>
  </span>
</div>
<div id="search-result">
  <div id="no-result">
    <i class="fa fa-spinner fa-pulse fa-5x fa-fw"></i>
  </div>
</div>

    </div>
  </div>

</div>
    </header>

    
  <div class="back-to-top">
    <i class="fa fa-arrow-up"></i>
    <span>0%</span>
  </div>


    <main class="main">
      <div class="main-inner">
        <div class="content-wrap">
          

          <div class="content post posts-expand">
            

    
  
  
  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="http://yang0033.gitee.io/2017/03/10/FRP-%E5%AE%9E%E4%BE%8B%E7%AE%80%E4%BB%8B(RxJS---RxSwift---ReactiveCocoa)/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="https://tva2.sinaimg.cn/crop.259.17.290.290.180/c2abdfa9jw8ez7appr3p2j20g40a2t91.jpg?KID=imgbed,tva&Expires=1594186820&ssig=BdxhDdi1Ti">
      <meta itemprop="name" content="SuperYang">
      <meta itemprop="description" content="牛肉炒饭不要香菜，老板收钱">
    </span>

    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="SuperYang`s Blog">
    </span>
      <header class="post-header">
        <h1 class="post-title" itemprop="name headline">
          FRP-实例简介(RxJS-RxSwift-ReactiveCocoa)
        </h1>

        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>

              <time title="创建时间：2017-03-10 00:24:00" itemprop="dateCreated datePublished" datetime="2017-03-10T00:24:00+08:00">2017-03-10</time>
            </span>
              <span class="post-meta-item">
                <span class="post-meta-item-icon">
                  <i class="far fa-calendar-check"></i>
                </span>
                <span class="post-meta-item-text">更新于</span>
                <time title="修改时间：2020-07-09 16:00:12" itemprop="dateModified" datetime="2020-07-09T16:00:12+08:00">2020-07-09</time>
              </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/categories/iOS/" itemprop="url" rel="index"><span itemprop="name">iOS</span></a>
                </span>
            </span>

          
            <span id="/2017/03/10/FRP-%E5%AE%9E%E4%BE%8B%E7%AE%80%E4%BB%8B(RxJS---RxSwift---ReactiveCocoa)/" class="post-meta-item leancloud_visitors" data-flag-title="FRP-实例简介(RxJS-RxSwift-ReactiveCocoa)" title="阅读次数">
              <span class="post-meta-item-icon">
                <i class="fa fa-eye"></i>
              </span>
              <span class="post-meta-item-text">阅读次数：</span>
              <span class="leancloud-visitors-count"></span>
            </span>
  
  <span class="post-meta-item">
    
      <span class="post-meta-item-icon">
        <i class="far fa-comment"></i>
      </span>
      <span class="post-meta-item-text">Valine：</span>
    
    <a title="valine" href="/2017/03/10/FRP-%E5%AE%9E%E4%BE%8B%E7%AE%80%E4%BB%8B(RxJS---RxSwift---ReactiveCocoa)/#valine-comments" itemprop="discussionUrl">
      <span class="post-comments-count valine-comment-count" data-xid="/2017/03/10/FRP-%E5%AE%9E%E4%BE%8B%E7%AE%80%E4%BB%8B(RxJS---RxSwift---ReactiveCocoa)/" itemprop="commentCount"></span>
    </a>
  </span>
  
  <br>
            <span class="post-meta-item" title="本文字数">
              <span class="post-meta-item-icon">
                <i class="far fa-file-word"></i>
              </span>
                <span class="post-meta-item-text">本文字数：</span>
              <span>20k</span>
            </span>
            <span class="post-meta-item" title="阅读时长">
              <span class="post-meta-item-icon">
                <i class="far fa-clock"></i>
              </span>
                <span class="post-meta-item-text">阅读时长 &asymp;</span>
              <span>18 分钟</span>
            </span>

        </div>
      </header>

    
    
    
    <div class="post-body" itemprop="articleBody">

      
        <h1 id="FRP是异步数据流编程"><a href="#FRP是异步数据流编程" class="headerlink" title="FRP是异步数据流编程"></a><a href="https://gist.github.com/staltz/868e7e9bc2a7b8c1f754#frp-is-programming-with-asynchronous-data-streams" target="_blank" rel="noopener">FRP是异步数据流编程</a></h1><hr>
<p>这不是什么新鲜的东西了。在前端编程中（用<code>Javascript</code>），监听某个按钮的点击事件，并在事件被触发以后回调一个函数做一些操作，这个过程就是异步数据流编程，也就是FRP。FRP的灵感来源于细胞的激素刺激，你可以回想一下初中生物学的“生物应激”。我们可以为任何东西创建数据流（Stream），不仅仅局限于click和hover事件。Stream是随处可见的，任何东西都可以成为Stream：变量、用户的输入、属性、缓存、数据结构等等。举个例子，微博的推荐（推荐好友，推荐好新闻）就是一种和click事件一样的Stream，你可以监听它的里面（推荐事件）并按需作出响应。<a id="more"></a><br>单一的一个Stream可以用来作为另一个Stream的输入，甚至多个Stream也可以输入给某一个Stream。假设现在你有一个超牛逼的工具集，包含了很多功能可以让你合并、创建和过滤Stream。那么你就可以<em>merge（合并）</em>多个Stream，对Stream做你只感兴趣的事件的<em>filter（过滤）</em>，也可以把一个Stream<em>map（映射）</em>为另外一个新的Stream。<br>既然Stream是FRP的核心，那我们就从最熟悉的“click a button”来仔细的了解Stream。<br><img src="https://camo.githubusercontent.com/36c0a9ffd8ed22236bd6237d44a1d3eecbaec336/687474703a2f2f692e696d6775722e636f6d2f634c344d4f73532e706e67" alt="zoom-图1"><br>从上面可以看出，Stream是<strong>一个不间断的按照时间顺序排列的event序列</strong>。它可以发出三样信号：值（value，对应于某些类型）、错误（error）和完成（completed）。只有当按钮所在的窗口或者视窗被关闭的时候，才会发出“完成”信号。<br>既然知道Stream会发出（emit）三种信号，那么我们就可以为其定义三个对应的执行函数异步的捕捉并处理这三种信号量。有时候，对于从Stream里面发出的error和completed，你可以按照需要选择捕捉处理或不捕捉处理。对Stream的“监听”叫做<strong>“订阅（subscribe）”</strong>，这些执行函数就是<strong>“观察者（observeers）”</strong>，Stream就是被观察的<strong>“主体（subject）”</strong>或者<strong>“可观察序列（observable）”</strong>。这正是<a href="https://en.wikipedia.org/wiki/Observer_pattern" target="_blank" rel="noopener">观察者模式</a>的设计体现。<br>在本教程的某些地方，我会用ASCII图来表示Stream，就叫做StreamGraph吧：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">--a---b-c---d---X---|-&gt;</span><br><span class="line">a, b, c, d ：事件对应的类型或者值</span><br><span class="line">X ：错误</span><br><span class="line">| ：完成</span><br><span class="line">---&gt; ：时间轴</span><br></pre></td></tr></table></figure>

<p>下面一起来做些有趣的尝试：把“Stream(origin click event)”转换为“Stream(counter click event)”。<br>使用FRP的时候，Stream会被拓展了很多方法，如<code>map</code><br>、<code>filter</code><br>、<code>scan</code><br>等。当调用其中一个方法时，如clickStream.map(f)<br>，它将返回一个<strong>“new Stream”</strong>。这意味着“origin Stream”没有发生改变。在FRP里，Stream具备<strong>恒定（immutability）</strong>的特性，说白了Stream是一个发生后即不可变的序列。所以clickStream.map(f).scan(g)<br>这样链式调用可以在Stream上操作。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">clickStream: ---c----c--c----c------c--&gt; </span><br><span class="line">                        vvvvv map(c becomes 1) vvvv</span><br><span class="line">                        ---1----1--1----1------1--&gt; </span><br><span class="line">                        vvvvvvvvv scan(+) vvvvvvvvv</span><br><span class="line">counterStream: ---1----2--3----4------5--&gt;</span><br></pre></td></tr></table></figure>
<p>上面的例子，map(f)<br>方法用传入的f<br>函数替代每个发出的信号量，把每次的点击事件映射为数值1。map<br>产生的流会被scan(g)<br>方法扫描并用g(accumulated, current)<br>来聚合，在例子中就是简单地相加。最后的countStream<br>统计了每次点击时，当前一共产生了多少次点击事件。<br>为了体现FRP的强大，假设现在需要一个double-click（双击）Stream（短时间内两次或三次的单击认为是双击）。深呼吸并回想你是怎么用传统的方式来实现的。我敢打赌一定让人非常抓狂，因为会需要大量的变量去描述每个阶段的状态，还会用到定时处理。<br>但用FRP会让事情变得非常简单。事实上，仅需要4行<a href="http://jsfiddle.net/staltz/4gGgs/27/" target="_blank" rel="noopener">逻辑代码</a>。不过，我们先忽略代码来看个图：<br><img src="https://camo.githubusercontent.com/995c301de2f566db10748042a5a67cc5d9ac45d9/687474703a2f2f692e696d6775722e636f6d2f484d47574e4f352e706e67" alt="图2"><br>不用需要理解底层是如何实现的，只管用就是了。灰色框里面的函数把一个Stram转换为另外一个Stream。先用buffer(stream.throttle(250ms))<br>判定出那些单击可以归为一次双击，从而获得一个新的归并后的单击Stream。然后用map()<br>做事件数量统计，获得一个新的含有每个归并单元中事件数量的Stream。最后用filter(x &gt;= 2)<br>来忽略数量为1<br>的归并单元。就这样，通过3步操作获得所需要的Stream。现在可以按照需要，通过subscribe对双击Stream进行监听并做出响应。<br>我希望你会享受这种编程方式。这个例子仅仅是冰山一角：你可以用FRP实现的类库如“Rx*”来做更多。</p>
<h1 id="为什么要用FRP？"><a href="#为什么要用FRP？" class="headerlink" title="为什么要用FRP？"></a><a href="https://gist.github.com/staltz/868e7e9bc2a7b8c1f754#why-should-i-consider-adopting-frp" target="_blank" rel="noopener">为什么要用FRP？</a></h1><p>FRP提高了抽象的层次，因此你可以专注于业务逻辑里面事件间的相互依赖，而不需要关心一大堆实现的细节。用FRP将会使代码变得更加简洁。</p>
<p>FRP的优势在富“UI事件和数据事件交互”的现代Web、移动应用得到了证明。10年前，Web页面的交互基本上就是提交一个大表单到后端，然后在前端执行简单的渲染。应用逐渐变得更加实时：修改单个表单域能够自动触发保存到后台，内容会根据个人的“喜好”匹配到相关用户等等。</p>
<p>现今的应用需要通过丰富多样的实时事件来提供高水平的用户体验，而FRP可以很好解决。</p>
<h1 id="实例讲解FRP"><a href="#实例讲解FRP" class="headerlink" title="实例讲解FRP"></a><a href="https://gist.github.com/staltz/868e7e9bc2a7b8c1f754#thinking-in-frp-with-examples" target="_blank" rel="noopener">实例讲解FRP</a></h1><p>让我们来点干货。通过一个真实的例子一步步的理解FRP。在教程的最后会给出所有的代码，同时了解每行代码在做什么。<br>我用Javascript和<a href="https://github.com/Reactive-Extensions/RxJS" target="_blank" rel="noopener">RxJS</a>作为工具，那是因为：Javascript是目前比较熟悉的语言，同时<a href="https://rx.codeplex.com/" target="_blank" rel="noopener">Rx*库系列</a>提供了对多种语言和平台的（<a href="https://rx.codeplex.com/" target="_blank" rel="noopener">.NET</a>，<a href="https://github.com/Netflix/RxJava" target="_blank" rel="noopener">Java</a>，<a href="https://github.com/ReactiveX/RxJava/tree/master/language-adaptors/rxjava-scala" target="_blank" rel="noopener">Scala</a>，<a href="https://github.com/Netflix/RxJava/tree/master/language-adaptors/rxjava-clojure" target="_blank" rel="noopener">Clojure</a>，<a href="https://github.com/Reactive-Extensions/RxJS" target="_blank" rel="noopener">JavaScript</a>，<a href="https://github.com/Reactive-Extensions/Rx.rb" target="_blank" rel="noopener">Ruby</a>，<a href="https://github.com/Reactive-Extensions/RxPy" target="_blank" rel="noopener">Python</a>，<a href="https://github.com/Reactive-Extensions/RxCpp" target="_blank" rel="noopener">C++</a>，<a href="https://github.com/ReactiveCocoa/ReactiveCocoa" target="_blank" rel="noopener">Objective-C/Cocoa</a>，<a href="https://github.com/Netflix/RxJava/tree/master/language-adaptors/rxjava-groovy" target="_blank" rel="noopener">Groovy</a>等等）支持。所以无论你用什么工具，都可以按照本教程享受FRP的好处。</p>
<h1 id="实现“推荐关注”"><a href="#实现“推荐关注”" class="headerlink" title="实现“推荐关注”"></a><a href="https://gist.github.com/staltz/868e7e9bc2a7b8c1f754#implementing-a-who-to-follow-suggestions-box" target="_blank" rel="noopener">实现“推荐关注”</a></h1><p>在微博里，有个专门推荐新用户给你关注的面板：<br><img src="https://camo.githubusercontent.com/81e5d63c69768e1b04447d2e246f47540dd83fbd/687474703a2f2f692e696d6775722e636f6d2f65416c4e62306a2e706e67" alt="图3"><br>这个“推荐关注”的实现包含以下这些核心特点：<br>启动的时候，从API中加载并显示3条其他账户信息。<br>点击“Refresh”按钮时，重新加载3条其他账户信息。<br>点击每条账户信息的“x”按钮时，清掉当前这条账户，并显示另外一条。</p>
<p>微博里面对未通过认证的开发着不公开这个“推荐关注”的API，因此我们用<a href="https://developer.github.com/v3/users/#get-all-users" target="_blank" rel="noopener">Github</a>的API来实现。</p>
<h1 id="请求和响应"><a href="#请求和响应" class="headerlink" title="请求和响应"></a><a href="https://gist.github.com/staltz/868e7e9bc2a7b8c1f754#request-and-response" target="_blank" rel="noopener">请求和响应</a></h1><p><strong>你会怎么用FRP来解决这个问题呢？</strong>嗯，开始的时候（几乎）<em>把任何东西都*</em>流化***。这几乎成了FRP的魔咒。我们从最简单的功能开始：“启动的时候，从API中加载并显示3条其他账户信息。”。这里没有任何问题，就是简单的(1)发送一个请求，(2)接收一个响应，(3)渲染这个响应。因此，我们用Stream来代表(1)的请求。起初这感觉像杀鸡用牛刀，但是我们需要从基本的东西开始，对吧？<br>启动的时候只需要发送一次请求，那么对应的StreamModel（流模型）是一个只含有一个值的Stream。最后会发现启动的时候会有很多请求，但目前只有一个：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">--a------|-&gt;</span><br><span class="line">a ：<span class="string">'https://api.github.com/users'</span>这个字符串</span><br></pre></td></tr></table></figure>

<p>这是一个请求地址（URL）Stream。这条Stream告诉了我们两件事情：“什么时候”和“是什么”（When &amp; What）。“什么时候”意味着当产生事件（emit event）时要发送请求，而“是什么”说明了产生的事件（emitted event）是一串请求地址字符。<br>用“Rx<em>”创建Stream是非常简单的。Stream的术语是</em>“Observable（可观察序列）”<em>，这说明它是可以被其他人观察的。但我发现这真是个愚蠢的名词，所以我更喜欢称它为</em>“Stream（流）”*。</p>
<p><strong>RxJS</strong></p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> requestStream = Rx.Observable.returnValue(<span class="string">'https://api.github.com/users'</span>);</span><br></pre></td></tr></table></figure>
<p>__ RxSwift__</p>
<figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> requestStream = <span class="type">Observable</span>.just(<span class="string">"https://api.github.com/users"</span>)</span><br></pre></td></tr></table></figure>

<p><strong>RAC</strong></p>
<figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">RACSignal *requestStream = [RACSignal <span class="keyword">return</span>:<span class="string">@"https://api.github.com/users"</span>];</span><br></pre></td></tr></table></figure>

<p>这里现在有一个只含一个字符串事件的Stream，但是没有任何操作，所以我们需要添加一个处理即将到来的字符串事件的函数。下面给requestStream<br>加上<a href="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypesubscribeobserver--onnext-onerror-oncompleted" target="_blank" rel="noopener">subscribing
</a>。<br><strong>RxJS</strong></p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">requestStream.subscribe(<span class="function"><span class="keyword">function</span>(<span class="params">requestUrl</span>)</span>&#123; </span><br><span class="line">        <span class="comment">// 发送请求 </span></span><br><span class="line">        jQuery.getJSON(requestUrl, <span class="function"><span class="keyword">function</span>(<span class="params">responseData</span>)</span>&#123; </span><br><span class="line">                   <span class="comment">// ...             </span></span><br><span class="line">        &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p><strong>RxSwift</strong></p>
<figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">requestStream.subscribe(onNext: &#123; (urlString) <span class="keyword">in</span></span><br><span class="line">            <span class="type">URLSession</span>.shared.dataTask(with: <span class="type">NSURL</span>(string: urlString) <span class="keyword">as</span>! <span class="type">URL</span>, completionHandler: &#123; (data, response, error) <span class="keyword">in</span></span><br><span class="line">  </span><br><span class="line">            &#125;).resume()</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p><strong>RAC</strong></p>
<figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">RACSignal *requestStream = [RACSignal <span class="keyword">return</span>:<span class="string">@"https://api.github.com/users"</span>];</span><br><span class="line">    [requestStream subscribeNext:^(<span class="keyword">id</span> x) &#123;</span><br><span class="line">       [[[<span class="built_in">NSURLSession</span> sharedSession] dataTaskWithURL:[<span class="built_in">NSURL</span> URLWithString:x] completionHandler:^(<span class="built_in">NSData</span> * _Nullable data, <span class="built_in">NSURLResponse</span> * _Nullable response, <span class="built_in">NSError</span> * _Nullable error) &#123;</span><br><span class="line">           </span><br><span class="line">       &#125;] resume];</span><br><span class="line">    &#125;];</span><br></pre></td></tr></table></figure>
<p>注意上面我们用了jQuery的Ajax<a href="http://devdocs.io/jquery/jquery.getjson" target="_blank" rel="noopener">回调</a>处理响应的结果。( ⊙ o ⊙ )！，等等，FRP不是擅长于处理异步数据流吗？能不能把jQuery的结果交给RxJS处理？感觉好像没什么问题，我们来试试：<br><strong>RxJS</strong></p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">requestStream.subscribe(<span class="function"><span class="keyword">function</span>(<span class="params">requestUrl</span>)</span>&#123; </span><br><span class="line">        <span class="comment">// 响应也是个流</span></span><br><span class="line">         <span class="keyword">var</span> responseStream = Rx.Observable.create(<span class="function"><span class="keyword">function</span>(<span class="params">observer</span>)</span>&#123; </span><br><span class="line">                   jQuery.getJSON(requestUrl) <span class="comment">// 当jQuery成功调用以后，就把结果交给RxJS处理 </span></span><br><span class="line">                    .done(<span class="function"><span class="keyword">function</span>(<span class="params">response</span>)</span>&#123; observer.onNext(response); &#125;) <span class="comment">// 当jQuery失败时，把失败交给RxJS处理 </span></span><br><span class="line">                    .fail(<span class="function"><span class="keyword">function</span>(<span class="params">jqXHR, status, error</span>)</span>&#123; observer.onError(error); &#125;) <span class="comment">// 当jQuery完成时，告知RxJS调用完成的处理 </span></span><br><span class="line">                    .always(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; observer.onCompleted(); &#125;) </span><br><span class="line">           &#125;); responseStream.subscribe(<span class="function"><span class="keyword">function</span>(<span class="params">response</span>)</span>&#123; </span><br><span class="line">                          <span class="comment">// 为响应做处理 </span></span><br><span class="line">            &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p><strong>RxSwift</strong></p>
<figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">requestStream.subscribe(onNext: &#123; (urlString) <span class="keyword">in</span></span><br><span class="line">            <span class="keyword">let</span> responseStream = <span class="type">Observable</span>&lt;<span class="type">Any</span>&gt;.create &#123; (observer) -&gt; <span class="type">Disposable</span> <span class="keyword">in</span></span><br><span class="line">                <span class="type">URLSession</span>.shared.dataTask(with: <span class="type">NSURL</span>(string:urlString ) <span class="keyword">as</span>! <span class="type">URL</span>, completionHandler: &#123; (data, response, error) <span class="keyword">in</span></span><br><span class="line">                    <span class="keyword">if</span> error != <span class="literal">nil</span> &#123;</span><br><span class="line">                        observer.onError(error!)</span><br><span class="line">                    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                        observer.onNext(data!)</span><br><span class="line">                    &#125;</span><br><span class="line">                    observer.onCompleted()</span><br><span class="line">                &#125;).resume()   </span><br><span class="line">                <span class="keyword">return</span> <span class="type">Disposables</span>.create()</span><br><span class="line">            &#125;</span><br><span class="line">            responseStream.subscribe &#123; (x) <span class="keyword">in</span></span><br><span class="line">                <span class="comment">// 响应处理</span></span><br><span class="line">                <span class="built_in">print</span>(x)</span><br><span class="line">            &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>
<p><strong>RAC</strong></p>
<figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">[requestStream subscribeNext:^(<span class="keyword">id</span> x) &#123;</span><br><span class="line">        [RACSignal createSignal:^RACDisposable *(<span class="keyword">id</span>&lt;RACSubscriber&gt; subscriber) &#123;</span><br><span class="line">            [[<span class="built_in">NSURLSession</span> sharedSession] dataTaskWithURL:[<span class="built_in">NSURL</span> URLWithString:x] completionHandler:^(<span class="built_in">NSData</span> * _Nullable data, <span class="built_in">NSURLResponse</span> * _Nullable response, <span class="built_in">NSError</span> * _Nullable error) &#123;</span><br><span class="line">                <span class="keyword">if</span> (error) &#123;</span><br><span class="line">                    [subscriber sendError:error];</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    [subscriber sendNext:data];</span><br><span class="line">                &#125;</span><br><span class="line">                [subscriber sendCompleted];</span><br><span class="line">            &#125;];</span><br><span class="line">            <span class="keyword">return</span> [RACDisposable disposableWithBlock:^&#123;</span><br><span class="line">                </span><br><span class="line">            &#125;];</span><br><span class="line">        &#125;];</span><br><span class="line">    &#125;];</span><br></pre></td></tr></table></figure>
<p>因为我们需要发送一个Ajax请求，所以我们就用jQuery包装一下我们的RxJS。上面看起来非常直观，而Rx.Observable.create()<br>通过传入一个包含observer参数的函数，会返回一个自定义的Stream。当Stream产生任何事件的时候，都会调用这个传入的方法，并传入当前observer。<strong>打扰一下，这是不是意味着Promise也是一个“Observable（可观察序列）”？</strong>【注：作者这里不用Stream，是为了更加官方的描述Promise。】</p>
<p>是的！<br>Observable是Promise++。在RxJS里面，你可以很容易的把Promise转换为Observable通过调用var stream = Rx.Observable.fromPromise(promise)<br>，所以我们来用用它。值得一提的是，Observable和<a href="http://promises-aplus.github.io/promises-spec/" target="_blank" rel="noopener">Promises/A+</a>是不兼容的，但概念上并没有冲突。你可以这样理解，Promise就是Observable的单值版本。</p>
<p>可以看到RxJS比起jQuery这类框架实现的Promise要强大多了。当别人大肆吹捧Promises的时候，你给给他说说RxJS。</p>
<p>好吧，回到我们的例子来。你注意到下面这些问题了吗？<br>把一个<code>subscribe()</code><br>调用嵌入了另外一个<code>subscribe()</code><br>里面，这可能会陷入<a href="http://callbackhell.com/" target="_blank" rel="noopener">“callback hell”</a>。<br>resposneStream<br>紧密依赖于requestStream<br>。【注：这里涉及“关注点分离”】</p>
<p>哎呀，那么多问题。幸亏，FRP提供了大量的操作函数来解决上面的问题。<br>现在相信你已经很清楚基础函数<code>map(f)</code>了。<br>这是一个把生产流（Stream A）里面的所有值你拿出来执行 <code>f</code> 转换，把转换的结果放入到消费流（Stream B）中。例如，我们正好需要把请求地址（URL）对应的转成一个响应的Stream（Promise可以包装成Stream）。<br><strong>RxJS</strong></p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> responseMetastream = requestStream</span><br><span class="line">         .map(<span class="function"><span class="keyword">function</span>(<span class="params">requestUrl</span>) </span>&#123;</span><br><span class="line">         <span class="keyword">return</span> Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p><strong>RxSwift</strong></p>
<figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> responseMetastream = requestStream</span><br><span class="line">        .<span class="built_in">map</span> &#123; (urlString: <span class="type">String</span>) -&gt; <span class="type">Observable</span>&lt;<span class="type">Any</span>&gt; <span class="keyword">in</span></span><br><span class="line">         <span class="keyword">return</span> <span class="type">URLSession</span>.shared.rx.json(url: <span class="type">NSURL</span>(string:urlString ) <span class="keyword">as</span>! <span class="type">URL</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><strong>RAC</strong></p>
<figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 2.5.0 还没出NSURLSession的扩展，先用NSURLConnetcion代替</span></span><br><span class="line">RACSignal *responseMetastream = [requestStream map:^<span class="keyword">id</span>(<span class="keyword">id</span> value) &#123;</span><br><span class="line">        <span class="keyword">return</span> [<span class="built_in">NSURLConnection</span> rac_sendAsynchronousRequest:[<span class="built_in">NSURLRequest</span> requestWithURL:[<span class="built_in">NSURL</span> URLWithString:value]]];</span><br><span class="line">    &#125;];</span><br></pre></td></tr></table></figure>
<p>不过，上面的代码创建了一个怪兽：<em>“metastream”</em>。“metastream”的每个值是一个Stream的指针：每个值指向另外一个Stream【注：map转换以后是流，但是流里面的东西是指向Promise的指针】。在我们的例子中，每个请求URL都被映射为一个指针指向对应包含响应的promise流。<br><img src="https://camo.githubusercontent.com/2a8a9cc75acd13443f588fd7f386bd7a6dcb271a/687474703a2f2f692e696d6775722e636f6d2f48486e6d6c61632e706e67" alt="zoom-图5"><br>响应的“metastream”让人看起来非常困惑，而且实际上我们需要的是一个包含Promise【注：Promise是流】的Stream，而不是一条包含Stream指针的“metastream”。向<a href="https://github.com/Reactive-Exatensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypeflatmapselector-resultselector" target="_blank" rel="noopener">Flatmap先生</a>说“你好”吧。flatmap()<br>是map()<br>的一个“扁平化”处理版本，就像是从“主干”流里分出“支流”，然后对“支流”处理。<strong>【注：flatmap和map的对比可以看<a href="https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables" target="_blank" rel="noopener">这里</a>，可以这样理解：map就是在源流的每个事件上用一个“返回值的函数”做了计算并返回值，然后组合再返回新的流。而flatmap是在源流的每个事件上用一个“会回流的函数”做了计算并返回流，然后把返回的流（子流）组合再返回新的流。】</strong>值得注意的时候，flatmap()<br> 不是在修复map()<br>，“metastream”也不是一个错误，它们都是真实的工具用于在FRP中解决异步响应的问题。</p>
<p><strong>RxJS</strong></p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> responseStream = requestStream</span><br><span class="line">        .flatmap(<span class="function"><span class="keyword">function</span>(<span class="params">requestUrl</span>)</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p><strong>RxSwift</strong></p>
<figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> responseStream = requestStream</span><br><span class="line">            .flatMap &#123; (urlString: <span class="type">String</span>) -&gt; <span class="type">Observable</span>&lt;<span class="type">Any</span>&gt; <span class="keyword">in</span></span><br><span class="line">                <span class="keyword">return</span> <span class="type">URLSession</span>.shared.rx.json(url: <span class="type">NSURL</span>(string:urlString ) <span class="keyword">as</span>! <span class="type">URL</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p><strong>RAC</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">RACSignal *responseStream &#x3D; [requestStream</span><br><span class="line">                                     flattenMap:^RACStream *(id value) &#123;</span><br><span class="line">        return [NSURLConnection rac_sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:value]]];</span><br><span class="line">&#125;];</span><br></pre></td></tr></table></figure>
<p><img src="https://camo.githubusercontent.com/0b0ac4a249e1c15d7520c220957acfece1af3e95/687474703a2f2f692e696d6775722e636f6d2f4869337a4e7a4a2e706e67" alt="zoom-图6"><br>很好。因为响应的Stream是基于请求的Stream而定义的，所以如果以后我们有更多的事件在请求的Stream中产生，就会有对应的事件在响应的Stream中产生。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">requestStream: --a-----b--c------------|-&gt;</span><br><span class="line">responseStream: -----A--------B-----C---|-&gt;</span><br><span class="line">（小写的是请求，大写的是响应）</span><br></pre></td></tr></table></figure>
<p>既然我们好不容易拥有了响应的Stream，那么我们就可以渲染所接收的数据：</p>
<p><strong>RxJS</strong></p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">responseStream.subscribe(<span class="function"><span class="keyword">function</span>(<span class="params">response</span>)</span>&#123; </span><br><span class="line">    <span class="comment">// 按照你的意愿在DOM树里面渲染response对象</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p><strong>RxSwift</strong></p>
<figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">responseStream.subscribe(onNext: &#123; (x) <span class="keyword">in</span></span><br><span class="line">      <span class="comment">// 按照你的意愿渲染View对象         </span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>
<p><strong>RAC</strong></p>
<figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">[responseStream subscribeNext:^(<span class="keyword">id</span> x) &#123;</span><br><span class="line">        <span class="comment">// 按照你的意愿渲染View对象   </span></span><br><span class="line">&#125;];</span><br></pre></td></tr></table></figure>
<p>我们把前面所有的代码合在一起，那样就是：</p>
<p><strong>RxJS</strong></p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> requestStream = Rx.Observable.returnValue(<span class="string">'https://api.github.com/users'</span>);</span><br><span class="line"><span class="keyword">var</span> responseStream = requestStream</span><br><span class="line">         .flatMap(<span class="function"><span class="keyword">function</span>(<span class="params">requestUrl</span>) </span>&#123; </span><br><span class="line">        <span class="keyword">return</span> Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));</span><br><span class="line"> &#125;);</span><br><span class="line">responseStream.subscribe(<span class="function"><span class="keyword">function</span>(<span class="params">response</span>) </span>&#123; </span><br><span class="line">        <span class="comment">// 按照你的意愿在DOM树里面渲染response对象</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p><strong>RxSwift</strong></p>
<figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> requestStream = <span class="type">Observable</span>.just(<span class="string">"https://api.github.com/users"</span>)</span><br><span class="line"><span class="keyword">let</span> responseStream = requestStream</span><br><span class="line">            .flatMap &#123; (urlString: <span class="type">String</span>) -&gt; <span class="type">Observable</span>&lt;<span class="type">Any</span>&gt; <span class="keyword">in</span></span><br><span class="line">                <span class="keyword">return</span> <span class="type">URLSession</span>.shared.rx.json(url: <span class="type">NSURL</span>(string:urlString ) <span class="keyword">as</span>! <span class="type">URL</span>)</span><br><span class="line">            &#125;</span><br><span class="line">responseStream.subscribe(onNext: &#123; (x) <span class="keyword">in</span></span><br><span class="line">              <span class="comment">// 按照你的意愿渲染View对象</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>
<p><strong>RAC</strong></p>
<figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">RACSignal *requestStream = [RACSignal <span class="keyword">return</span>:<span class="string">@"https://api.github.com/users"</span>];</span><br><span class="line">RACSignal *responseStream = [requestStream</span><br><span class="line">                                     flattenMap:^RACStream *(<span class="keyword">id</span> value) &#123;</span><br><span class="line">        <span class="keyword">return</span> [<span class="built_in">NSURLConnection</span> rac_sendAsynchronousRequest:[<span class="built_in">NSURLRequest</span> requestWithURL:[<span class="built_in">NSURL</span> URLWithString:value]]];</span><br><span class="line">&#125;];</span><br><span class="line">[responseStream subscribeNext:^(<span class="keyword">id</span> x) &#123;</span><br><span class="line">        <span class="comment">// 按照你的意愿渲染View对象</span></span><br><span class="line">&#125;];</span><br></pre></td></tr></table></figure>
<h1 id="刷新按钮"><a href="#刷新按钮" class="headerlink" title="刷新按钮"></a><a href="https://gist.github.com/staltz/868e7e9bc2a7b8c1f754#the-refresh-button" target="_blank" rel="noopener">刷新按钮</a></h1><p>我没有提及一件事情就是上面的响应返回的JSON制式的用户信息有100条。这个API仅仅允许我们传页偏移值，而不允许传页限制数，所以导致我们只能用3条数据对象而浪费97条。我们现在先忽略这些问题，后面将会看到如何缓存这些响应。<br>每次刷新按钮被点击的时候，请求的Stream就会产生一个String事件。我们需要两样东西：<br>刷新按钮上产生点击事件Stream；<br>上述的刷新按钮的点击事件Stream可以改变请求的Stream。</p>
<p>可喜的是，RxJS具备相应的工具给DOM元素构建指定的事件的Stream：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> refreshButton = <span class="built_in">document</span>.querySelector(<span class="string">'.refresh'</span>);</span><br><span class="line"><span class="keyword">var</span> refreshClickStream = Rx.Observable.fromEvent(refreshButton, <span class="string">'click'</span>);</span><br></pre></td></tr></table></figure>

<p>接下来，让刷新按钮的点击事件Stream改变请求的Stream。通过传一个每次都随机产生的参数作为偏移值发送请求给Github：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> requestStream = refreshClickStream</span><br><span class="line">        .map(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="keyword">var</span> randomOffset = <span class="built_in">Math</span>.floor(<span class="built_in">Math</span>.random() * <span class="number">500</span>); </span><br><span class="line">        <span class="keyword">return</span> <span class="string">'https://api.github.com/users?since='</span> + randomOffset;</span><br><span class="line"> &#125;);</span><br></pre></td></tr></table></figure>
<p>不过现在有个问题，就是请求在启动的时候并不会马上被发送，只会在刷新按钮被点击时才会执行。如何才能在启动的时候马上发送请求并且点击刷新按钮的时候也能发送请求？<br>首先，我们都知道如何为上面说的两种情况创建对应的Stream：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> requestOnRefreshStream = refreshClickStream</span><br><span class="line">        .map(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123; </span><br><span class="line">        <span class="keyword">var</span> randomOffset = <span class="built_in">Math</span>.floor(<span class="built_in">Math</span>.random()*<span class="number">500</span>); </span><br><span class="line">        <span class="keyword">return</span> <span class="string">'https://api.github.com/users?since='</span> + randomOffset; </span><br><span class="line">&#125;);</span><br><span class="line"><span class="keyword">var</span> startupRequestStream =Rx.Observable.returnValue(<span class="string">'https://api.github.com/users'</span>);</span><br></pre></td></tr></table></figure>
<p>但是如何“合并”上面这两个Stream为一个Stream呢？不用担心，这里有merge()<br>。用StreamGraph来描述：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">stream A: ---a--------e-----o-----&gt;</span><br><span class="line">stream B: -----B---C-----D--------&gt; </span><br><span class="line">        vvvvvvvvv merge vvvvvvvvv </span><br><span class="line">        ---a-B---C--e--D--o-----&gt;</span><br></pre></td></tr></table></figure>

<p>现在事情就变得简单了：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> requestOnRefreshStream = refreshClickStream </span><br><span class="line">        .map(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123; </span><br><span class="line">        <span class="keyword">var</span> randomOffset = <span class="built_in">Math</span>.floor(<span class="built_in">Math</span>.random()*<span class="number">500</span>);</span><br><span class="line">        <span class="keyword">return</span> <span class="string">'https://api.github.com/users?since='</span> + randomOffset;</span><br><span class="line"> &#125;);</span><br><span class="line"><span class="keyword">var</span> startupRequestStream = Rx.Observable.returnValue(<span class="string">'https://api.github.com/users'</span>);</span><br><span class="line"><span class="keyword">var</span> requestStream = Rx.Observable.merge(</span><br><span class="line">        requestOnRefreshStream, startupRequestStream</span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>这里有另外一个干净简单的方式去书写上面的代码：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> requestStream = refreshClickStream </span><br><span class="line">        .map(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123; </span><br><span class="line">        <span class="keyword">var</span> randomOffset = <span class="built_in">Math</span>.floor(<span class="built_in">Math</span>.random()*<span class="number">500</span>);</span><br><span class="line">        <span class="keyword">return</span> <span class="string">'https://api.github.com/users?since='</span> + randomOffset;</span><br><span class="line"> &#125;) </span><br><span class="line">.merge(Rx.Observable.returnValue(<span class="string">'https://api.github.com/users'</span>));</span><br></pre></td></tr></table></figure>
<p>甚至更短，可读性更强：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> requestStream = refreshClickStream </span><br><span class="line">        .map(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123; </span><br><span class="line">        <span class="keyword">var</span> randomOffset = <span class="built_in">Math</span>.floor(<span class="built_in">Math</span>.random()*<span class="number">500</span>); </span><br><span class="line">        <span class="keyword">return</span> <span class="string">'https://api.github.com/users?since='</span> + randomOffset; </span><br><span class="line">&#125;) </span><br><span class="line">.startWith(<span class="string">'https://api.github.com/users'</span>);</span><br></pre></td></tr></table></figure>
<p>这个startWith()方法恰好精准的反映了你想要做的事情。无论你传入的Stream是什么样子的，但最后调用startWith(x)<br>，就会以x作为开始。不过这不够<a href="https://en.wikipedia.org/wiki/Don' target="_blank" rel="noopener"t_repeat_yourself">DRY</a>，我重复了访问Github的请求地址。解决这个问题的方法就是通过移动startWith()<br>到refreshClickStream后，然后在启动时“模拟”一次刷新点击：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> requestStream = refreshClickStream.startWith(<span class="string">'startup click'</span>) </span><br><span class="line">      .map(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">           <span class="keyword">var</span> randomOffset = <span class="built_in">Math</span>.floor(<span class="built_in">Math</span>.random()*<span class="number">500</span>); </span><br><span class="line">           <span class="keyword">return</span> <span class="string">'https://api.github.com/users?since='</span> + randomOffset; </span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<h1 id="给“推荐关注”的每项建模"><a href="#给“推荐关注”的每项建模" class="headerlink" title="给“推荐关注”的每项建模"></a><a href="https://gist.github.com/staltz/868e7e9bc2a7b8c1f754#modelling-the-3-suggestions-with-streams" target="_blank" rel="noopener">给“推荐关注”的每项建模</a></h1><p>现在，我们只能在responseStream的subscribe()里，才能够对每项推荐的UI元素做渲染操作。可是，如果你用最快的速度点击刷新按钮的时候，当前的3条推荐都没有被清掉，而新的推荐只有在请求到达以后才会到达。这就看起来好像是点了刷新和不点刷新没有两样似的。但为了让UI看起来更舒服点，我们需要在点击刷新时清除当前的3条推荐。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">refreshClickStream.subscribe(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">	<span class="comment">// 清除3条推荐的DOM元素</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>如果你这么干，现在就会有<em>两个</em>订阅者（一个是<code>refreshClickStream.subscribe()</code><br>，另外一个是<code>responseStream.subscribe()</code><br>）关联着这3条推荐的DOM元素，事情会变得很糟糕。因为这不是<a href="https://en.wikipedia.org/wiki/Separation_of_concerns" target="_blank" rel="noopener">“关注点分离”</a>【注：关注点分离是指只对与“特定概念、目标”（关注点）相关联的软件组成部分进行“标识、封装和操纵”的能力。这是处理复杂性的一个原则。因为关注点混杂在一起将会加大软件的复杂度，而分离开关注点进行处理能够降解复杂度。面向切面编程的核心就是关注点分离。】。<br><img src="https://camo.githubusercontent.com/e581baffb3db3e4f749350326af32de8d5ba4363/687474703a2f2f692e696d6775722e636f6d2f4149696d5138432e6a7067" alt="图7"><br>因此我们需要给每项推荐做Stream处理，并使得每个事件都包含响应JSON值。我们将会针对3条推荐中的每条做分离。第1条推荐分离后的样子：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> suggestion1Stream = responseStream</span><br><span class="line">  .map(<span class="function"><span class="keyword">function</span>(<span class="params">listUsers</span>) </span>&#123;</span><br><span class="line">    <span class="comment">// 随机从列表中获取一个</span></span><br><span class="line">    <span class="keyword">return</span> listUsers[<span class="built_in">Math</span>.floor(<span class="built_in">Math</span>.random()*listUsers.length)];</span><br><span class="line">  &#125;);</span><br></pre></td></tr></table></figure>

<p>对此，<code>suggestion2Stream</code>和<code>suggestion3Stream</code>可以简单的从suggestion1Stream里面复制过来。虽然这不够DRY，不过保证了我们的例子足够的简单。</p>
<p>取代<code>resposneStream.subscribe()</code>里面的渲染操作，我们可以这样做：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">suggestion1Stream.subscribe(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">	<span class="comment">// 渲染第1个推荐到DOM</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>回到“在刷新的时候，清除所有推荐”，我们可以通过映射刷新点击到null的推荐数据，那么在<code>suggestion1Stream</code>里面，就像：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> suggestion1Stream = responseStream.map(<span class="function"><span class="keyword">function</span>(<span class="params">listUsers</span>)</span>&#123;</span><br><span class="line">	<span class="keyword">return</span> listUsers[<span class="built_in">Math</span>.floor(<span class="built_in">Math</span>.random() * listUsers.length)];</span><br><span class="line">&#125;)</span><br><span class="line">.merge(</span><br><span class="line">	refreshClickStream.map(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; <span class="keyword">return</span> <span class="literal">null</span>; &#125;)</span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>在渲染的时候，我们把null解析为“没有数据”，从而隐藏对应的UI元素。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">suggestion1Stream.subscribe(<span class="function"><span class="keyword">function</span>(<span class="params">suggestion</span>)</span>&#123;</span><br><span class="line">	<span class="keyword">if</span> (suggestion === <span class="literal">null</span>) &#123;</span><br><span class="line">		<span class="comment">// 隐藏第1个推荐的DOM元素</span></span><br><span class="line">	&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">		<span class="comment">// 或者展示第1个推荐的DOM元素并渲染对应的数据</span></span><br><span class="line">	&#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>那么现在对应的流图：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">refreshClickStream: ----------o--------o----&gt;</span><br><span class="line">     requestStream: -r--------r--------r----&gt;</span><br><span class="line">    responseStream: ----R---------R------R--&gt;</span><br><span class="line"> suggestion1Stream: ----s-----N---s----N-s--&gt;</span><br><span class="line"> suggestion2Stream: ----q-----N---q----N-q--&gt;</span><br><span class="line"> suggestion3Stream: ----t-----N---t----N-t--&gt;</span><br><span class="line"></span><br><span class="line"> N ：代表null</span><br></pre></td></tr></table></figure>
<p>为了更完善，我们也可以在开始的时候渲染“空”推荐。通过添加startWith(null)到第一条推荐的Stream：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> suggestion1Stream = responseStream</span><br><span class="line">  .map(<span class="function"><span class="keyword">function</span>(<span class="params">listUsers</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> listUsers[<span class="built_in">Math</span>.floor(<span class="built_in">Math</span>.random()*listUsers.length)];</span><br><span class="line">  &#125;)</span><br><span class="line">  .merge(</span><br><span class="line">    refreshClickStream.map(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; <span class="keyword">return</span> <span class="literal">null</span>; &#125;)</span><br><span class="line">  )</span><br><span class="line">  .startWith(<span class="literal">null</span>);</span><br></pre></td></tr></table></figure>
<p>最终的StreamGraph如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">refreshClickStream: ----------o---------o----&gt;</span><br><span class="line">     requestStream: -r--------r---------r----&gt;</span><br><span class="line">    responseStream: ----R----------R------R--&gt;</span><br><span class="line"> suggestion1Stream: -N--s-----N----s----N-s--&gt;</span><br><span class="line"> suggestion2Stream: -N--q-----N----q----N-q--&gt;</span><br><span class="line"> suggestion3Stream: -N--t-----N----t----N-t--&gt;</span><br><span class="line"></span><br><span class="line"> N ：代表null</span><br></pre></td></tr></table></figure>
<h1 id="关闭一个推荐以及使用缓存结果集"><a href="#关闭一个推荐以及使用缓存结果集" class="headerlink" title="关闭一个推荐以及使用缓存结果集"></a><a href="https://gist.github.com/staltz/868e7e9bc2a7b8c1f754#closing-a-suggestion-and-using-cached-responses" target="_blank" rel="noopener">关闭一个推荐以及使用缓存结果集</a></h1><p>还有最后一个功能还未实现：每个推荐都会有对应的“x”按钮用于清除当前的推荐并加载新的推荐。刚开始弄的时候，你可能会选择在关闭某个推荐的时候发起一个新的请求：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">var close1Button &#x3D; document.querySelector(&#39;.close1&#39;);</span><br><span class="line">var close1ClickStream &#x3D; Rx.Observable.fromEvent(close1Button, &#39;click&#39;);</span><br><span class="line">&#x2F;&#x2F; 同理于 close2Button和close3Button</span><br><span class="line">var requestStream &#x3D; refreshClickStream.startWith(&#39;startup click&#39;)</span><br><span class="line">  .merge(close1ClickStream) &#x2F;&#x2F; 我们添加了这个，使得点击close1的时候，会触发新的请求</span><br><span class="line">  .map(function() &#123;</span><br><span class="line">    var randomOffset &#x3D; Math.floor(Math.random()*500);</span><br><span class="line">    return &#39;https:&#x2F;&#x2F;api.github.com&#x2F;users?since&#x3D;&#39; + randomOffset;</span><br><span class="line">  &#125;);</span><br></pre></td></tr></table></figure>
<p>但如果我们点击任意一个关闭按钮的时候，它会清除当前所有的推荐并重新加载。有很多方法可以解决，为了让事情更有趣，我们将会重用上次请求后响应的数据去解决这个问题。Github的响应中每页的大小为100个用户信息，然而我们只需要使用到其中的3个，因此会存在大量有效的新数据。不需要再发起新的请求。</p>
<p>我们再把它想象成为<code>Stream</code>。当第一条推荐的“关闭”按钮被点击时，我们需要在<code>resposneStream</code>中上一个的响应数据中随机获取一个用户数据。就像：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">requestStream:     --r---------------&gt;</span><br><span class="line">responseStream:    ------R-----------&gt;</span><br><span class="line">close1ClickStream: ------------c-----&gt;</span><br><span class="line">suggestion1Stream: ------s-----s-----&gt;</span><br><span class="line"></span><br><span class="line"> c ：代表关闭</span><br></pre></td></tr></table></figure>
<p>在“Rx*”里面有一个联合函数<code>combineLatest</code>看起来可以实现我们的需求。它把两个不同<code>Stream</code>作为输入，无论其中哪个<code>Stream</code>产生一个事件，<code>combineLatest</code>会组合两个<code>Stream</code>的“上一个”事件，以参数<code>a</code>和<code>b</code>的形式然后输出值<code>c = f(x,y)</code>，而<code>f</code>是你所定义的函数。用StreamGraph解析：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">stream A: --a-----------e--------i--------&gt;</span><br><span class="line">stream B: -----b----c--------d-------q----&gt;</span><br><span class="line">          vvvvvvvv combineLatest(f) vvvvvvv</span><br><span class="line">          ----AB---AC--EC---ED--ID--IQ----&gt;</span><br><span class="line"></span><br><span class="line">f ：大写函数</span><br></pre></td></tr></table></figure>
<p>我们可以在<code>close1ClickStream</code>上调用<code>combineLatest()</code>，传入<code>responseStream</code>。这样当点击“关闭”按钮1时，我们都会获得<code>responseStream</code>的上一个事件并计算出新值给<code>suggestion1Stream</code>。另外一方面<code>combineLatest()</code>函数是对称的：<code>responseStream</code>产生新的事件会组合“关闭”按钮1的上一个事件，计算出新值传给<code>suggestion1Stream</code>。这样我们就可以简化之前<code>suggestion1Stream</code>的代码:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> suggestion1Stream = close1ClickStream</span><br><span class="line">  .combineLatest(responseStream,</span><br><span class="line">    <span class="function"><span class="keyword">function</span>(<span class="params">click, listUsers</span>) </span>&#123;</span><br><span class="line">      <span class="keyword">return</span> listUsers[<span class="built_in">Math</span>.floor(<span class="built_in">Math</span>.random()*listUsers.length)];</span><br><span class="line">    &#125;</span><br><span class="line">  )</span><br><span class="line">  .merge(</span><br><span class="line">    refreshClickStream.map(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; <span class="keyword">return</span> <span class="literal">null</span>; &#125;)</span><br><span class="line">  )</span><br><span class="line">  .startWith(<span class="literal">null</span>);</span><br></pre></td></tr></table></figure>
<p>但这里还有一个问题：<code>combineLatest()</code>使用最近两个源，但是如果其中一个没有产生事件，那么组合的<code>Stream</code>（<code>suggestion1Stream</code>）是不会产生事件的。认真观察前面的<code>StreamGraph</code>，你会发现当A流产生a事件时，<code>suggestion1Stream</code>不会产生事件。只有在B流产生b事件的时候，组合的<code>Stream</code>才会产生事件。</p>
<p>我们用最简单的方法来解决这个问题，就是在启动时“模拟”点击了’关闭’按钮1：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> suggestion1Stream = close1ClickStream.startWith(<span class="string">'startup click'</span>) <span class="comment">// 我们增加了这个</span></span><br><span class="line">  .combineLatest(responseStream,</span><br><span class="line">    <span class="function"><span class="keyword">function</span>(<span class="params">click, listUsers</span>) </span>&#123;l</span><br><span class="line">      <span class="keyword">return</span> listUsers[<span class="built_in">Math</span>.floor(<span class="built_in">Math</span>.random()*listUsers.length)];</span><br><span class="line">    &#125;</span><br><span class="line">  )</span><br><span class="line">  .merge(</span><br><span class="line">    refreshClickStream.map(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; <span class="keyword">return</span> <span class="literal">null</span>; &#125;)</span><br><span class="line">  )</span><br><span class="line">  .startWith(<span class="literal">null</span>);</span><br></pre></td></tr></table></figure>
<h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a><a href="https://gist.github.com/staltz/868e7e9bc2a7b8c1f754#wrapping-up" target="_blank" rel="noopener">总结</a></h1><p>终于弄完了。下面是全部代码</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> refreshButton = <span class="built_in">document</span>.querySelector(<span class="string">'.refresh'</span>);</span><br><span class="line"><span class="keyword">var</span> closeButton1 = <span class="built_in">document</span>.querySelector(<span class="string">'.close1'</span>);</span><br><span class="line"><span class="keyword">var</span> closeButton2 = <span class="built_in">document</span>.querySelector(<span class="string">'.close2'</span>);</span><br><span class="line"><span class="keyword">var</span> closeButton3 = <span class="built_in">document</span>.querySelector(<span class="string">'.close3'</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> refreshClickStream = Rx.Observable.fromEvent(refreshButton, <span class="string">'click'</span>);</span><br><span class="line"><span class="keyword">var</span> close1ClickStream = Rx.Observable.fromEvent(closeButton1, <span class="string">'click'</span>);</span><br><span class="line"><span class="keyword">var</span> close2ClickStream = Rx.Observable.fromEvent(closeButton2, <span class="string">'click'</span>);</span><br><span class="line"><span class="keyword">var</span> close3ClickStream = Rx.Observable.fromEvent(closeButton3, <span class="string">'click'</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> requestStream = refreshClickStream.startWith(<span class="string">'startup click'</span>)</span><br><span class="line">    .map(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> randomOffset = <span class="built_in">Math</span>.floor(<span class="built_in">Math</span>.random()*<span class="number">500</span>);</span><br><span class="line">        <span class="keyword">return</span> <span class="string">'https://api.github.com/users?since='</span> + randomOffset;</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> responseStream = requestStream</span><br><span class="line">    .flatMap(<span class="function"><span class="keyword">function</span> (<span class="params">requestUrl</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> Rx.Observable.fromPromise($.getJSON(requestUrl));</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">createSuggestionStream</span>(<span class="params">closeClickStream</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> closeClickStream.startWith(<span class="string">'startup click'</span>)</span><br><span class="line">        .combineLatest(responseStream,</span><br><span class="line">            <span class="function"><span class="keyword">function</span>(<span class="params">click, listUsers</span>) </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> listUsers[<span class="built_in">Math</span>.floor(<span class="built_in">Math</span>.random()*listUsers.length)];</span><br><span class="line">            &#125;</span><br><span class="line">        )</span><br><span class="line">        .merge(</span><br><span class="line">            refreshClickStream.map(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">                <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">            &#125;)</span><br><span class="line">        )</span><br><span class="line">        .startWith(<span class="literal">null</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> suggestion1Stream = createSuggestionStream(close1ClickStream);</span><br><span class="line"><span class="keyword">var</span> suggestion2Stream = createSuggestionStream(close2ClickStream);</span><br><span class="line"><span class="keyword">var</span> suggestion3Stream = createSuggestionStream(close3ClickStream);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">//渲染 ---------------------------------------------------</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">renderSuggestion</span>(<span class="params">suggestedUser, selector</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> suggestionEl = <span class="built_in">document</span>.querySelector(selector);</span><br><span class="line">    <span class="keyword">if</span> (suggestedUser === <span class="literal">null</span>) &#123;</span><br><span class="line">        suggestionEl.style.visibility = <span class="string">'hidden'</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        suggestionEl.style.visibility = <span class="string">'visible'</span>;</span><br><span class="line">        <span class="keyword">var</span> usernameEl = suggestionEl.querySelector(<span class="string">'.username'</span>);</span><br><span class="line">        usernameEl.href = suggestedUser.html_url;</span><br><span class="line">        usernameEl.textContent = suggestedUser.login;</span><br><span class="line">        <span class="keyword">var</span> imgEl = suggestionEl.querySelector(<span class="string">'img'</span>);</span><br><span class="line">        imgEl.src = <span class="string">""</span>;</span><br><span class="line">        imgEl.src = suggestedUser.avatar_url;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">suggestion1Stream.subscribe(<span class="function"><span class="keyword">function</span> (<span class="params">suggestedUser</span>) </span>&#123;</span><br><span class="line">    renderSuggestion(suggestedUser, <span class="string">'.suggestion1'</span>);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">suggestion2Stream.subscribe(<span class="function"><span class="keyword">function</span> (<span class="params">suggestedUser</span>) </span>&#123;</span><br><span class="line">    renderSuggestion(suggestedUser, <span class="string">'.suggestion2'</span>);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">suggestion3Stream.subscribe(<span class="function"><span class="keyword">function</span> (<span class="params">suggestedUser</span>) </span>&#123;</span><br><span class="line">    renderSuggestion(suggestedUser, <span class="string">'.suggestion3'</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>上面的代码非常简短，但是十分紧凑：它体现了使用适当的关注点分离甚至响应捕获可以控制符合的事件。函数化的代码风格使得其看起来像描述而不是编码：我们从来没有给出一系列的执行过程，仅仅是通过定义流之间的关系来描述这是什么。例如，通过<strong>FRP</strong>，我们告诉计算机<code>suggestion1Stream</code>是“关闭”按钮1的<code>Stream</code>和上一次请求的响应的<code>Stream</code>做组合，当刷新发生或者程序启动的时候会变成<code>null</code>。</p>
<p>值得一提的是，上面的代码既没有诸如<code>if</code>、<code>for</code>、<code>while</code>这类流程控制元素，也没有经典的回调控制流。通过使用<code>subscribe()</code>和<code>filter()</code>，你终于可以摆脱<code>if</code>和<code>else</code>了。</p>
<p>如果你认为“Rx<em>”会成为FRP的首选库，那么花些时间去<a href="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md" target="_blank" rel="noopener">看看</a>如何用函数来转换、组合和创建Observables。如果你想通过StreamGraph的形式来理解这些函数，你可以访问<a href="https://github.com/ReactiveX/RxJava/wiki/Creating-Observables" target="_blank" rel="noopener">这个地址</a>。这就是我开发的经验总结：当你使用FRP时有任何疑问，可以先画些图再思考如何解决。<br>一旦你着手开始用“Rx</em>”进行编程的时候，非常有必要理解<a href="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/creating.md#cold-vs-hot-observables" target="_blank" rel="noopener">“冷和热的可观察序列”</a>【注：“冷”是指只有订阅者需要的时候才从Stream里面产生一个事件，而且订阅者之间没有任何关联。“热”是指Stream会自动产生事件，而订阅者之间存在关联。而我们上面的例子中，Stream是个“冷”序列】。如果你忽略这个，后面可能会遇到很多奇怪的问题。你已经被提醒了。在日后通过学习真实的函数编程来磨练你的能力，并且理解“Rx<em>”所带来的其他副作用。<br>不仅仅只有“Rx</em>”可以实现FRP。还有<a href="http://baconjs.github.io/" target="_blank" rel="noopener">Bacon.js</a>，<a href="http://elm-lang.org/" target="_blank" rel="noopener">Elm</a>等。<br>FRP在富事件的Web前端和移动应用中有着不俗的问题解决能力。但它不仅仅只适用于客户端，它也可在后端工作和访问数据库。事实上，<a href="http://techblog.netflix.com/2013/02/rxjava-netflix-api.html" target="_blank" rel="noopener">RxJava是Netflix后端服务里面一个非常重要的组件</a>。请记住，FRP不是一个基于某种语言或者应用的框架，它是一种应用于事件驱动的编程范式。</p>

    </div>

    
    
    
        

  <div class="followme">
    <p>欢迎关注我的其它发布渠道</p>

    <div class="social-list">

        <div class="social-item">
          <a target="_blank" class="social-link" href="https://www.jianshu.com/u/f37a8f0ba6f8">
            <span class="icon">
              <i class="fas fa-book"></i>
            </span>

            <span class="label">简书</span>
          </a>
        </div>
    </div>
  </div>


      <footer class="post-footer">
          <div class="post-tags">
              <a href="/tags/RAC/" rel="tag"># RAC</a>
              <a href="/tags/FRP/" rel="tag"># FRP</a>
              <a href="/tags/RxJS/" rel="tag"># RxJS</a>
              <a href="/tags/RxSwift/" rel="tag"># RxSwift</a>
          </div>

        


        
    <div class="post-nav">
      <div class="post-nav-item">
    <a href="/2017/03/03/%E5%88%A9%E7%94%A8-RACObserve-%E5%8F%82%E6%95%B0%E4%BA%8C%E6%8E%A7%E5%88%B6%E7%9B%91%E5%90%AC%E8%8C%83%E5%9B%B4/" rel="prev" title="利用-RACObserve-参数二控制监听范围">
      <i class="fa fa-chevron-left"></i> 利用-RACObserve-参数二控制监听范围
    </a></div>
      <div class="post-nav-item">
    <a href="/2017/07/24/JS-%E9%AA%A8%E9%AA%BC%E5%8A%A8%E7%94%BB-(DragonBones)-%E5%9C%A8-iOS-%E7%AB%AF%E6%88%AA%E5%B1%8F%E5%8A%9F%E8%83%BD/" rel="next" title="JS-骨骼动画-(DragonBones)-在-iOS-端截屏功能">
      JS-骨骼动画-(DragonBones)-在-iOS-端截屏功能 <i class="fa fa-chevron-right"></i>
    </a></div>
    </div>
      </footer>
    
  </article>
  
  
  



          </div>
          
    <div class="comments" id="valine-comments"></div>

<script>
  window.addEventListener('tabs:register', () => {
    let { activeClass } = CONFIG.comments;
    if (CONFIG.comments.storage) {
      activeClass = localStorage.getItem('comments_active') || activeClass;
    }
    if (activeClass) {
      let activeTab = document.querySelector(`a[href="#comment-${activeClass}"]`);
      if (activeTab) {
        activeTab.click();
      }
    }
  });
  if (CONFIG.comments.storage) {
    window.addEventListener('tabs:click', event => {
      if (!event.target.matches('.tabs-comment .tab-content .tab-pane')) return;
      let commentClass = event.target.classList[1];
      localStorage.setItem('comments_active', commentClass);
    });
  }
</script>

        </div>
          
  
  <div class="toggle sidebar-toggle">
    <span class="toggle-line toggle-line-first"></span>
    <span class="toggle-line toggle-line-middle"></span>
    <span class="toggle-line toggle-line-last"></span>
  </div>

  <aside class="sidebar">
    <div class="sidebar-inner">

      <ul class="sidebar-nav motion-element">
        <li class="sidebar-nav-toc">
          文章目录
        </li>
        <li class="sidebar-nav-overview">
          站点概览
        </li>
      </ul>

      <!--noindex-->
      <div class="post-toc-wrap sidebar-panel">
          <div class="post-toc motion-element"><ol class="nav"><li class="nav-item nav-level-1"><a class="nav-link" href="#FRP是异步数据流编程"><span class="nav-number">1.</span> <span class="nav-text">FRP是异步数据流编程</span></a></li><li class="nav-item nav-level-1"><a class="nav-link" href="#为什么要用FRP？"><span class="nav-number">2.</span> <span class="nav-text">为什么要用FRP？</span></a></li><li class="nav-item nav-level-1"><a class="nav-link" href="#实例讲解FRP"><span class="nav-number">3.</span> <span class="nav-text">实例讲解FRP</span></a></li><li class="nav-item nav-level-1"><a class="nav-link" href="#实现“推荐关注”"><span class="nav-number">4.</span> <span class="nav-text">实现“推荐关注”</span></a></li><li class="nav-item nav-level-1"><a class="nav-link" href="#请求和响应"><span class="nav-number">5.</span> <span class="nav-text">请求和响应</span></a></li><li class="nav-item nav-level-1"><a class="nav-link" href="#刷新按钮"><span class="nav-number">6.</span> <span class="nav-text">刷新按钮</span></a></li><li class="nav-item nav-level-1"><a class="nav-link" href="#给“推荐关注”的每项建模"><span class="nav-number">7.</span> <span class="nav-text">给“推荐关注”的每项建模</span></a></li><li class="nav-item nav-level-1"><a class="nav-link" href="#关闭一个推荐以及使用缓存结果集"><span class="nav-number">8.</span> <span class="nav-text">关闭一个推荐以及使用缓存结果集</span></a></li><li class="nav-item nav-level-1"><a class="nav-link" href="#总结"><span class="nav-number">9.</span> <span class="nav-text">总结</span></a></li></ol></div>
      </div>
      <!--/noindex-->

      <div class="site-overview-wrap sidebar-panel">
        <div class="site-author motion-element" itemprop="author" itemscope itemtype="http://schema.org/Person">
    <img class="site-author-image" itemprop="image" alt="SuperYang"
      src="https://tva2.sinaimg.cn/crop.259.17.290.290.180/c2abdfa9jw8ez7appr3p2j20g40a2t91.jpg?KID=imgbed,tva&Expires=1594186820&ssig=BdxhDdi1Ti">
  <p class="site-author-name" itemprop="name">SuperYang</p>
  <div class="site-description" itemprop="description">牛肉炒饭不要香菜，老板收钱</div>
</div>
<div class="site-state-wrap motion-element">
  <nav class="site-state">
      <div class="site-state-item site-state-posts">
          <a href="/archives/">
        
          <span class="site-state-item-count">39</span>
          <span class="site-state-item-name">日志</span>
        </a>
      </div>
      <div class="site-state-item site-state-categories">
            <a href="/categories/">
          
        <span class="site-state-item-count">5</span>
        <span class="site-state-item-name">分类</span></a>
      </div>
      <div class="site-state-item site-state-tags">
            <a href="/tags/">
          
        <span class="site-state-item-count">31</span>
        <span class="site-state-item-name">标签</span></a>
      </div>
  </nav>
</div>
  <div class="links-of-author motion-element">
      <span class="links-of-author-item">
        <a href="https://github.com/yangchao0033" title="GitHub → https:&#x2F;&#x2F;github.com&#x2F;yangchao0033" rel="noopener" target="_blank"><i class="fab fa-github fa-fw"></i>GitHub</a>
      </span>
      <span class="links-of-author-item">
        <a href="mailto:ygrfwyc@gmail.com" title="E-Mail → mailto:ygrfwyc@gmail.com" rel="noopener" target="_blank"><i class="fa fa-envelope fa-fw"></i>E-Mail</a>
      </span>
      <span class="links-of-author-item">
        <a href="https://weibo.com/yangchao0033" title="Weibo → https:&#x2F;&#x2F;weibo.com&#x2F;yangchao0033" rel="noopener" target="_blank"><i class="fab fa-weibo fa-fw"></i>Weibo</a>
      </span>
      <span class="links-of-author-item">
        <a href="https://www.jianshu.com/u/f37a8f0ba6f8" title="简书 → https:&#x2F;&#x2F;www.jianshu.com&#x2F;u&#x2F;f37a8f0ba6f8" rel="noopener" target="_blank"><i class="fas fa-book fa-fw"></i>简书</a>
      </span>
      <span class="links-of-author-item">
        <a href="https://t.me/superYang0033" title="Telegram → https:&#x2F;&#x2F;t.me&#x2F;superYang0033" rel="noopener" target="_blank"><i class="fab fa-telegram fa-fw"></i>Telegram</a>
      </span>
  </div>



      </div>

      
        <div id="music163player">
        <iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=350 height=86 src="//music.163.com/outchain/player?type=2&id=31473269&auto=0&height=66">
        </iframe>
        </div>
    </div>

    

  </aside>
  <div id="sidebar-dimmer"></div>


      </div>
    </main>

    <footer class="footer">
      <div class="footer-inner">
        

        

<div class="copyright">
  
  &copy; 
  <span itemprop="copyrightYear">2020</span>
  <span class="with-love">
    <i class="fa fa-heart"></i>
  </span>
  <span class="author" itemprop="copyrightHolder">SuperYang</span>
    <span class="post-meta-divider">|</span>
    <span class="post-meta-item-icon">
      <i class="fa fa-chart-area"></i>
    </span>
    <span title="站点总字数">157k</span>
    <span class="post-meta-divider">|</span>
    <span class="post-meta-item-icon">
      <i class="fa fa-coffee"></i>
    </span>
    <span title="站点阅读时长">2:23</span>
</div>
  <div class="powered-by">由 <a href="https://hexo.io/" class="theme-link" rel="noopener" target="_blank">Hexo</a> & <a href="https://theme-next.org/" class="theme-link" rel="noopener" target="_blank">NexT.Gemini</a> 强力驱动
  </div>

        








      </div>
    </footer>
  </div>

  
  <script src="/lib/anime.min.js"></script>
  <script src="/lib/velocity/velocity.min.js"></script>
  <script src="/lib/velocity/velocity.ui.min.js"></script>

<script src="/js/utils.js"></script>

<script src="/js/motion.js"></script>


<script src="/js/schemes/pisces.js"></script>


<script src="/js/next-boot.js"></script>




  
  <script>
    (function(){
      var canonicalURL, curProtocol;
      //Get the <link> tag
      var x=document.getElementsByTagName("link");
		//Find the last canonical URL
		if(x.length > 0){
			for (i=0;i<x.length;i++){
				if(x[i].rel.toLowerCase() == 'canonical' && x[i].href){
					canonicalURL=x[i].href;
				}
			}
		}
    //Get protocol
	    if (!canonicalURL){
	    	curProtocol = window.location.protocol.split(':')[0];
	    }
	    else{
	    	curProtocol = canonicalURL.split(':')[0];
	    }
      //Get current URL if the canonical URL does not exist
	    if (!canonicalURL) canonicalURL = window.location.href;
	    //Assign script content. Replace current URL with the canonical URL
      !function(){var e=/([http|https]:\/\/[a-zA-Z0-9\_\.]+\.baidu\.com)/gi,r=canonicalURL,t=document.referrer;if(!e.test(r)){var n=(String(curProtocol).toLowerCase() === 'https')?"https://sp0.baidu.com/9_Q4simg2RQJ8t7jm9iCKT-xh_/s.gif":"//api.share.baidu.com/s.gif";t?(n+="?r="+encodeURIComponent(document.referrer),r&&(n+="&l="+r)):r&&(n+="?l="+r);var i=new Image;i.src=n}}(window);})();
  </script>




  
<script src="/js/local-search.js"></script>













  

  

  

  <script async src="/js/cursor/fireworks.js"></script>



<script>
NexT.utils.loadComments(document.querySelector('#valine-comments'), () => {
  NexT.utils.getScript('//unpkg.com/valine/dist/Valine.min.js', () => {
    var GUEST = ['nick', 'mail', 'link'];
    var guest = 'nick,mail,link';
    guest = guest.split(',').filter(item => {
      return GUEST.includes(item);
    });
    new Valine({
      el         : '#valine-comments',
      verify     : true,
      notify     : true,
      appId      : 'zizRqalUJY55Xc5oBJKbhxpV-gzGzoHsz',
      appKey     : '64gPcVi8lIA8zVGpBSEk7uKu',
      placeholder: "留言板无需注册登录，快来评论吧。。",
      avatar     : 'mm',
      meta       : guest,
      pageSize   : '10' || 10,
      visitor    : true,
      lang       : '' || 'zh-cn',
      path       : location.pathname,
      recordIP   : false,
      serverURLs : ''
    });
  }, window.Valine);
});
</script>

</body>
</html>
